/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package poproject.GUI;

import JaCoP.core.IntDomain;
import JaCoP.core.IntVar;
import java.awt.*;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.geom.Point2D;
import java.awt.geom.RoundRectangle2D;
import java.beans.PropertyChangeSupport;
import java.util.ArrayList;
import javax.swing.*;
import poproject.OptimizationTask.MapModel;
import poproject.OptimizationTask.MapObstacle;
import poproject.OptimizationTask.MapTrack;
import poproject.OptimizationTask.OptimizationTaskEngine;

/**
 * Kontrolka do GUI wizualizująca mapkę do zadania optymalizacji
 *
 * @author Eruanno
 */
public class MapVisualizer extends JComponent 
implements ComponentListener, MouseListener {
    
    public final static int DEFAULT_PADDING = 10;
    
    public final static Color DEFAULT_FOREGROUND_COLOR = new Color(120, 235, 120);
    public final static Color DEFAULT_BACKGROUND_COLOR = new Color(200, 255, 200);
    
    // Kolory
    private Color gridColor = new Color(80, 200, 80); 
    private Color obstaclePointFill = new Color(200, 50, 25);
    private Color emptyPointFill = new Color(150, 225, 150);
    private Color trackPointFill = new Color(255, 155, 50);
    private Color startFinishFill = new Color(200, 200, 255);
    
    private Rectangle drawArea; ///< Obszar rysowania mapy
    
    // Aktualny model mapy
    private MapModel mapModel;
    
    // Wcięcie względem granic kontrolki
    private int padding;
    
    @Override
    public void mouseClicked(MouseEvent e) {
        Point p = e.getPoint();
        
        p.translate(-drawArea.x, -drawArea.y);
        double relX = (double)drawArea.width / mapModel.numOfAreas();
        double relY = (double)drawArea.height / (mapModel.numOfLevels()-1);
        
        int clickedPos = (int)Math.round((double)p.x / relX);
        int clickedLevel = (int)Math.round((double)p.y / relY);
        
        if(clickedPos < 0) clickedPos = 0;
        else if(clickedPos > mapModel.numOfAreas()) clickedPos = mapModel.numOfAreas();
        
        if(clickedLevel < 0) clickedLevel = 0;
        else if(clickedLevel > mapModel.numOfLevels()-1) clickedLevel = mapModel.numOfLevels()-1;
        
        mapModel.toggleObstacle(clickedPos, clickedLevel);
        this.repaint();
    }

    @Override
    public void mousePressed(MouseEvent e) {
        
    }

    @Override
    public void mouseReleased(MouseEvent e) {
    }

    @Override
    public void mouseEntered(MouseEvent e) {
    }

    @Override
    public void mouseExited(MouseEvent e) {
    }
    
    /**
     * Klasa pomocnicza przechowująca dane dot. rysowania
     */
    private class DrawingContext {
        public int nL;
        public int nN;
        public double offsetX;
        public double offsetY;
        private Shape clipRegion;
    }

    /**
     * Konstruktor domyślny.
     */
    public MapVisualizer() {
        setForeground(DEFAULT_FOREGROUND_COLOR);
        setBackground(DEFAULT_BACKGROUND_COLOR);
        
        padding = DEFAULT_PADDING;
        
        addComponentListener(this);
        
        // Wymuś odświeżenie zmiennej drawArea
        componentResized(null);
        addMouseListener(this);
    }            

    /**
     * Odrysowuje kontrolkę w GUI.
     * 
     * @param graphics Obiekt grafiki.
     */
    @Override
    public void paintComponent(Graphics graphics){
        Graphics2D g = (Graphics2D) graphics;
        
        DrawingContext dc = new DrawingContext();
        
        // Grubość kreski
        g.setStroke(new BasicStroke(2.0f));
        
        // Tło (obły prostokąt)
        g.setColor(this.getBackground());
        g.fillRoundRect(padding, padding, 
                this.getWidth() - 2*padding, this.getHeight() - 2*padding, 20, 20);
        g.setColor(this.getForeground());
        g.drawRoundRect(padding, padding, 
                this.getWidth() - 2*padding, this.getHeight() - 2*padding, 20, 20);
       
        // Ustaw maskę rysowania
        dc.clipRegion = new RoundRectangle2D.Double(drawArea.x, drawArea.y,
                drawArea.width, drawArea.height, 20, 20);
        g.drawRoundRect(drawArea.x, drawArea.y,
                drawArea.width, drawArea.height, 20, 20);
        g.setClip(dc.clipRegion);
        
        // Nie rysuj nic więcej jeśli nie ma przypisanego modelu mapy;        
        if(mapModel == null)
            return;
        
        // Ustaw popularne wartości:
        dc.nN = mapModel.numOfAreas();
        dc.nL = mapModel.numOfLevels();  
        dc.offsetX = (double)drawArea.width / dc.nN;
        dc.offsetY = (double)drawArea.height / (dc.nL-1);
 
        // Rysuj siatkę
        g.setColor(gridColor);
        drawGrid(g, dc);
             
        // Rysuj wszystkie przeszkody
        for(int i = 0; i <= mapModel.numOfAreas(); i++) {
            drawObstacle(g, dc, mapModel.obstacle(i));
        } 
        
        // Rysuj ścieżkę
        drawTrack(g, dc, mapModel.currentTrack());
    }   
    
    /**
     * Rysuje siatkę.
     * @param g
     * @param dc 
     */
    private void drawGrid(Graphics2D g, DrawingContext dc) {
        // Linie pionowe
        for(int i = 1; i < mapModel.numOfAreas(); i++) {
            g.drawLine((int)(drawArea.x + dc.offsetX * i), drawArea.y, 
                    (int)(drawArea.x + dc.offsetX * i), drawArea.y + drawArea.height);
        } 
        // Linie poziome
        for(int i = 1; i < dc.nL-1; i++) {
            g.drawLine( drawArea.x, (int)(drawArea.y + dc.offsetY*i), 
                    drawArea.x + drawArea.width, (int)(drawArea.y + dc.offsetY*i) );
        }
        // Punkty
        g.setClip(null);
        for (int i = 0; i <= dc.nN; i++) {
            for (int j = 0; j < dc.nL; j++) {
                drawPoint(g, dc, i, j, emptyPointFill, 6);
            }
        }
        g.setClip(dc.clipRegion);
    }
    
    /**
     * Rysuje przeszkodę.
     * @param g
     * @param dc
     * @param obst 
     */
    private void drawObstacle(Graphics2D g, DrawingContext dc, MapObstacle obst) {
        if(obst == null)
            return;
        
        int pos = obst.position();
        ArrayList<Integer> restricted = obst.restrictedCoords();
        
        double dX = dc.offsetX / 2 + 1;
        double dY = dc.offsetY / 2 + 1;
        
        g.setClip(null);
        for(Integer r : restricted) {
//            Point2D.Double p = this.calculateCoords(dc, pos, r);
//            g.fillRect((int) (p.x - dX), (int) (p.y - dY),
//                    (int) (2 * dX), (int) (2 * dY));
            drawPoint(g, dc, pos, r, obstaclePointFill, 3);
        }
        g.setClip(dc.clipRegion);
    }
    
    private void drawTrack(Graphics2D g, DrawingContext dc, MapTrack track) {
        if(track == null) return;
        
        // Linia
        for(int i=0; i < dc.nN; i++) {
            Point2D.Double p1 = calculateCoords(dc, i, track.trackPoint(i));
            Point2D.Double p2 = calculateCoords(dc, i+1, track.trackPoint(i+1));
            g.drawLine((int)p1.x, (int)p1.y, (int)p2.x, (int)p2.y);
        }
        
        g.setClip(null);
        for(int i=1; i < dc.nN; i++) {
            drawPoint(g, dc, i, track.trackPoint(i), trackPointFill, 4);
        }
        // Rysuj punkty startowy i końcowy
        drawPoint(g, dc, 0, track.trackPoint(0), startFinishFill, 3);
        drawPoint(g, dc, dc.nN, track.trackPoint(dc.nN), startFinishFill, 3);
        
        g.setClip(dc.clipRegion);
    }
    
    // Oblicza współrzędne w pikselach na podstawie numeru pozycji i poziomu
    private Point2D.Double calculateCoords(DrawingContext dc, int position, int level) {
        int L = mapModel.numOfLevels();
        int N = mapModel.numOfAreas();
        
        double x = (double)drawArea.width/N * position + drawArea.x;
        double y = (double)drawArea.height/(L-1) * level + drawArea.y;
        
        return new Point2D.Double(x, y);
    }
    
    // Rysuje kółko :)
    private void drawPoint(Graphics2D g, DrawingContext dc, int position, int level, Color c, int div) {
        Point2D.Double p = calculateCoords(dc, position, level);
        
        double scale = dc.offsetX / div;
        g.setColor(c);
        g.fillOval((int)(p.x - scale), (int)(p.y - scale), (int)(2*scale), (int)(2*scale));
        g.setColor(c.darker());
        g.drawOval((int)(p.x - scale), (int)(p.y - scale), (int)(2*scale), (int)(2*scale));
    }
    
    /**
     * Ustawia aktualny model mapy.
     * @param model Model mapy
     */
    public void setMapModel(MapModel model) {
        mapModel = model;
        this.invalidate();
    }
    
    public MapModel mapModel() { return mapModel ; }
    
    /**
     * Zdarzenie wywołane gdy zmianie ulegnie rozmiar kontrolki
     * @param e
     */
    @Override
    public void componentResized(ComponentEvent e){
        drawArea = new Rectangle(10, 10, this.getWidth()-20, this.getHeight()-20);
        this.repaint();
    }
    
    @Override
    public void componentMoved(ComponentEvent e) {
        //throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
    }

    @Override
    public void componentShown(ComponentEvent e) {
        //throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
    }

    @Override
    public void componentHidden(ComponentEvent e) {
        //throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
    }
    
    private void drawCurve(Graphics2D g) {
       Shape p;
    }
   
    
    //////// TEST AREA //////////
    /**
     * Służy do testów kontrolki.
     *
     * @param args
     */
    public static void main(String[] args) {
        JFrame frame = new JFrame("Test Frame");  // Create a window to display component.
        frame.setSize(400, 400);                  // Set a default window size.
        frame.setLocationRelativeTo(null);        // Brings the window to the center of the screen.

        frame.setDefaultCloseOperation( // Tells the program to terminate if the
                JFrame.EXIT_ON_CLOSE);                  // close [x] button is pressed.

        MapVisualizer myMap = new MapVisualizer();
        frame.getContentPane().add(myMap);

        MapModel mp = new MapModel(15, 10);
        MapObstacle obst = new MapObstacle(mp, 7);
        obst.addRestricted(1); obst.addRestricted(2);obst.addRestricted(3);
        obst.addRestricted(7); obst.addRestricted(8);
        obst.addRestricted(0);
        
        MapObstacle obst2 = new MapObstacle(mp, 2);
        obst2.addRestricted(3); obst2.addRestricted(4);obst2.addRestricted(5);

        mp.addObstacle(obst);
        mp.addObstacle(obst2);
        

        
        mp.setStartLevel(2); mp.setFinishLevel(8);
       
        for(int i = 0; i < mp.numOfAreas(); i++)
            mp.setAreaCost(i, i+1);
              
        OptimizationTaskEngine ote = new OptimizationTaskEngine(mp);
        ote.createModel();
        
        if(ote.searchSolution())
        {
            System.out.println("Znaleziono rozwiązanie");
        }
        else
        {
            System.out.println("Nieznaleziono rozwiązania");
        }
        
        MapTrack mt = ote.getTrackSolution();
        mp.setCurrentTrack(mt);
               
        myMap.setMapModel(mp);
        
        MapRecalcListener testListener = new MapRecalcListener(myMap);
        
        frame.setVisible(true);                   // Show the window.
    }
    
    private static class MapRecalcListener implements MouseListener {

        MapVisualizer mv;
        public MapRecalcListener(MapVisualizer v) { mv = v; v.addMouseListener(this);}
        @Override
        public void mouseClicked(MouseEvent e) {
            OptimizationTaskEngine ote = new OptimizationTaskEngine(mv.mapModel());
            ote.createModel();

            if(ote.searchSolution())
            {
                System.out.println("Znaleziono rozwiązanie");
            }
            else
            {
                System.out.println("Nieznaleziono rozwiązania");
            }

            MapTrack mt = ote.getTrackSolution();
            mv.mapModel().setCurrentTrack(mt);
            mv.repaint();
        }

        @Override
        public void mousePressed(MouseEvent e) {
        }

        @Override
        public void mouseReleased(MouseEvent e) {
        }

        @Override
        public void mouseEntered(MouseEvent e) {
        }

        @Override
        public void mouseExited(MouseEvent e) {
        }
  
    }
}